<?php
namespace app\command;


use app\command\extend\http\ProfitStatistics;
use app\common\model\coin\CoinRuleRunTrade;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;

class Crontab extends Command
{


    /**
     * 配置信息
     */
    const CONFIG = [
        'RESTART_LOG_NAME'  => 'restart.log',
        'ERROR_LOG_NAME'  => 'error.log',
        'ID_TO_CMDNAME' => [    // ID 对应的 cmd 名 // [名,类型（ALONE=>独立进程,SHARE=>共享进程）]
            1   => ['TRADE_SERVER','ALONE'], // 下单监控
            2   => ['LOG','SHARE'], // 下单监控
            3   => ['UDP_SERVER','SHARE'],  //备份下单监控
            4   => ['HTTP_SERVER','SHARE'],  //http服务接口
            5   => ['MONITOR_ACCOUNT','SHARE'],  //余额变化监控
            6   => ['MONITOR_TRADE','SHARE'],  //订单详情监控
            7   => ['HISTORY_TRADE','ALONE'],  //行情监控
        ],

        'ID_TO_CRONTAB' => [ // 定时器 ID 对应 名及定时 [名称,定时时间单位 秒]
            1   => ['STATISTICS_ONISSIONS',5],    // 将待整理的进行整理进报表
            2   => ['STATISTICS_ARRANGE',1800],    // 整理报表
            3   => ['TRADE_FINISH_ERROR_ARRANGE',60],  //整理请求第三方没返回完成的订单
        ],
    ];

    /**
     * ENV 配置参数KEY列表
     */
    const ENV_KEY_LIST = [
        'OPEN_CMD_ID_LIST'  => 'crontab.cmd_id_list',   //开启cmd的id列表
        'OPEN_CRONTAB_ID_LIST' => 'crontab.crontab_id_list',    //开启定时器的列表
    ];


    /**
     * 间隔时间执行，单位秒
     */
    const USLEEP_TIME_LIST = [
        'VALIDATE_LIST'    => [ // 验证时长
            'DEFAULT'   => 0, // 默认验证时长 0 ,表示只验证一次
            'TRADE_SERVER'   => 1500000, // 验证时长 1.5 s
            'UDP_SERVER'   => 1500000, // 验证时长 1.5 s
            'HTTP_SERVER'   => 1500000, // 验证时长 1.5 s
        ],

        'SHARE' => 200000,  // 共享监控循环周期

        'WAIT' => [    //独立执行等待时长
            'DEFAULT'   => 500000, //  默认 0.5 s
            'LOG'   => 10000000,  // 日志等待时长 10S
            'TRADE_SERVER'   => 10000000,  // 下单服务器监控 10S
            'UDP_SERVER'    => 10000000,    //10 s
            'HTTP_SERVER'    => 30000000,    //30 s
            'MONITOR_ACCOUNT'    => 10000000,    //10 s
            'MONITOR_TRADE'    => 10000000,    //10 s
            'HISTORY_TRADE'    => 10000000,    //10 s
        ],

        'WHILE_LIST'    => [    // 循环等待时间
            'DEFAULT'   => 500000, // 间隔时长 0.5 s
        ],
    ];

    // 获取监控验证地址
    private $validatePath = null;

    /**
     * @var null CMD 列表
     */
    static private $cmdList = null;



    /**
     * 重写configure
     * {@inheritdoc}
     */
    protected function configure()
    {
        // 命令的名字（"think" 后面的部分）
        $this->setName('Crontab')
            // 运行 "php think list" 时的简短描述
            ->setDescription('定时执行处理')
            // 运行命令时使用 "--help" 选项时的完整命令描述
            ->setHelp("一直执行 无参数");
    }


    /**
     *  * 重写execute
     *  * {@inheritdoc}
     *
     * @param Input $input
     * @param Output $output
     */
    public function execute(Input $input, Output $output)
    {
        // 判断是否开启了进程
        $isWait = false;

        // 设置存放地址
        $this->setValidatePath();

        // 启用监控
        $isWait = $this->startMonitor() || $isWait;


        // 开启定时器
        $isWait = $this->startCrontab() || $isWait;


        //开启等待
        if($isWait) {
            Swoole::wait();
        }

        return true;
    }


    /**
     * 获取指令名对应指令列表
     * @param null $cmdName 指令名
     * @return mixed|string|string[]|null
     */
    static function getCmdList($cmdName = null) {
        if(is_null(self::$cmdList)) {
            $path = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR;
            $cmd = 'php ' . $path . 'think ';
            self::$cmdList = [
                'TRADE_SERVER'   => $cmd . 'TradeServer',
                'LOG'   => $cmd . 'Log',
                'UDP_SERVER'   => $cmd . 'UdpServer',
                'HTTP_SERVER'   => $cmd . 'HttpServer',
                'MONITOR_ACCOUNT'   => $cmd . 'MonitorAccount',
                'MONITOR_TRADE'   => $cmd . 'MonitorTrade',
                'HISTORY_TRADE'   => $cmd . 'HistoryTrade 0',
            ];
        }
        return $cmdName?self::$cmdList[$cmdName]:self::$cmdList;
    }


    /**
     * 获取监控进程列表
     */
    private function getMonitorList() {
        $typeIdList = $this->env(self::ENV_KEY_LIST['OPEN_CMD_ID_LIST'],implode(',',array_keys(self::CONFIG['ID_TO_CMDNAME'])),',');
        $list = [
            'ALONE' => [],
            'SHARE' => [],
        ];
        foreach($typeIdList as $id) {
            if(array_key_exists($id,self::CONFIG['ID_TO_CMDNAME'])) {
                $cmdDetails = self::CONFIG['ID_TO_CMDNAME'][$id];
                $cmdName = $cmdDetails[0];
                // 通过判断是否存在进行验证
                $validateFunc = function($validateData) use($cmdName){
                    $cmd = self::getCmdList($cmdName);
                    if(exec('ps -ef | grep \'' . $cmd . '\' | wc -l') - 2 > 0) {
                        return self::validateWrite($validateData);
                    }
                    return true;
                };
                switch ($cmdDetails[0]) {
                    case 'TRADE_SERVER':    // 监控验证函数
                        $validateFunc = function ($validateData) {
                            return TradeServer::send($validateData);
                        };
                        break;
                    case 'UDP_SERVER':
                        $validateFunc = function ($validateData) {
                            return UdpServer::request(UdpServer::CMD_LIST['VALIDATE'],'127.0.0.1:' . UdpServer::serverPort(),$validateData);
                        };
                        break;
                    case 'HTTP_SERVER':
                        $validateFunc = function ($validateData) {
                            try {
                                // 判断盈利计算是否正常，无法正常允许则重启
                                if((new CoinRuleRunTrade())->where([
                                    ['statistics','=',1],
                                    ['update_time','<',time() - 20],
                                ])->count()) {
                                    return false;
                                }
                            } catch (\Exception $e) {
                            }
                            return HttpServer::request(HttpServer::CMD_LIST['VALIDATE'],$validateData,'127.0.0.1:' . HttpServer::serverPort());
                        };
                        break;
                    case 'HISTORY_TRADE':   // 行情监控
                        $validateFunc = function($validateData) use($cmdName){
                            $cmd = self::getCmdList($cmdName);
                            if(exec('ps -ef | grep \'' . $cmd . '\' | wc -l') - 2 > 0 && !exec($cmd . ' checkUpRestart')) {
                                return self::validateWrite($validateData);
                            }
                            return true;
                        };
                        break;
                }

                $list[$cmdDetails[1]][] = [$cmdDetails[0],$validateFunc,bcdiv(self::getUtime($cmdDetails[0],'VALIDATE'),1000000,4) - 0];
            }
        }
        return $list;
    }


    /**
     * 获取定时器列表
     */
    private function getCrontabList() {
        $typeIdList = $this->env(self::ENV_KEY_LIST['OPEN_CRONTAB_ID_LIST'],implode(',',array_keys(self::CONFIG['ID_TO_CRONTAB'])),',');
        $result = [];
        foreach ($typeIdList as $id) {
            if(array_key_exists($id,self::CONFIG['ID_TO_CRONTAB'])) {
                $crontab = self::CONFIG['ID_TO_CRONTAB'][$id];
                switch ($crontab[0]) {
                    case 'STATISTICS_ONISSIONS':    // 定时执行表数据
                        $crontab[] = function () {
                            ProfitStatistics::request('Switch',[
                                'type'   => 'omissions',
                                'id'=> 0
                            ]);
                        };
                        break;
                    case 'STATISTICS_ARRANGE':  // 定时执行转换数据
                        $crontab[] = function () {
                            ProfitStatistics::request('Switch',[
                                'type'   => 'arrange',
                                'id'=> 0
                            ]);
                        };
                        break;
                    case 'TRADE_FINISH_ERROR_ARRANGE': //整理请求第三方没返回完成的订单
                        $crontab[] = function () {
                            try {
                                Db::execute("
                                UPDATE b_coin_rule_run
                                INNER JOIN b_coin_rule ON b_coin_rule.coin_rule_run_id = b_coin_rule_run.id
                                INNER JOIN b_coin_rule_run_trade ON b_coin_rule_run_trade.coin_rule_run_id = b_coin_rule_run.id 
                                SET 
                                b_coin_rule_run.unique_id = CONCAT(b_coin_rule_run_trade.coin_rule_id,'_',b_coin_rule_run_trade.coin_rule_run_id),
                                b_coin_rule_run.deviations_quantity = 0,
                                b_coin_rule_run.reserved_quantity = b_coin_rule_run_trade.reserved_quantity,
                                b_coin_rule_run.reserved_price = b_coin_rule_run_trade.reserved_price,
                                b_coin_rule_run.update_time=b_coin_rule_run_trade.update_time,
                                
                                b_coin_rule.last_coin_rule_run_id=b_coin_rule_run.id,
                                b_coin_rule.coin_rule_run_id=0
                                WHERE
                                    b_coin_rule_run_trade.update_time >UNIX_TIMESTAMP(now()) - 60 * 60 * 12 
                                    AND b_coin_rule_run_trade.type = 2 
                                    AND b_coin_rule_run_trade.`status` = 5 
                                    AND b_coin_rule_run.unique_id = CONCAT( b_coin_rule_run.coin_rule_id, '' )
                            ");
                            } catch (\Exception $e) {
                            }
                        };

                        break;
                }
                if(array_key_exists(2,$crontab)) {
                    $result[] = $crontab;
                }
            }
        }
        return $result;
    }



    /**
     * 开启监控处理
     */
    private function startMonitor() {
        // 获取监控列表
        $monitorList = $this->getMonitorList();
        if($monitorList['ALONE']) { // 判断独立监控
            foreach($monitorList['ALONE'] as list($cmdName,$validateFunc,$validateTime)) {
                $aloneUtime = self::getUtime($cmdName,'WAIT');
                // 开启进程，并进行实时监控
                Swoole::startProcessAndErrorRestart(function() use($cmdName,$validateFunc,$validateTime,$aloneUtime){
                    try {
                        $this->monitor($cmdName,$validateFunc,$validateTime);
                    }catch (\Exception $e) {
                        $this->errorLog($cmdName,$e);
                    }
                    usleep($aloneUtime);
                });

            }
        }

        if($monitorList['SHARE']) { // 判断共享监控
            $shareRunList = [];
            $shareUtime = self::USLEEP_TIME_LIST['SHARE'];
            $shareList = $monitorList['SHARE'];
            foreach($shareList as &$details) {
                $details[] = bcdiv(self::getUtime($details[0],'WAIT'),1000000,4) - 0;
                $shareRunList[$details[0]] = 0;
            }
            Swoole::startProcessAndErrorRestart(function() use($shareList,$shareUtime,&$shareRunList){
                foreach($shareList as list($cmdName,$validateFunc,$validateTime,$waitUtime)) {
                    if(microtime(true) - $shareRunList[$cmdName] < $waitUtime) {
                        continue;
                    }
                    try {
                        $this->monitor($cmdName,$validateFunc,$validateTime);
                    }catch (\Exception $e) {
                        $this->errorLog($cmdName,$e);
                    }
                    $shareRunList[$cmdName] = microtime(true);
                }
                usleep($shareUtime);
            });
        }
        return empty($monitorList['ALONE']) && empty($monitorList['SHARE'])?false:true;
    }

    /**
     * 开启定时器
     */
    private function startCrontab() {
        if(!$crontabList = $this->getCrontabList()) {
            return false;   // 无需执行定时器
        }
        $whileUtime = self::getUtime('','WHILE');
        $runTimeList = [];
        foreach ($crontabList as $crontab) {
            $runTimeList[$crontab[0]] = 0;
        }
        Swoole::startProcessAndErrorRestart(function() use($whileUtime,$crontabList,&$runTimeList){

            foreach($crontabList as list($crontabName,$waitTime,$runFunc)) {
                if(microtime(true) - $runTimeList[$crontabName] < $waitTime) {
                    continue;
                }
                try {
                    $runFunc();
                }catch (\Exception $e) {
                    $this->errorLog('CRONTAB_' . $crontabName,$e);
                }
                $runTimeList[$crontabName] = microtime(true);
            }
            usleep($whileUtime);
        });

        return true;
    }




    /**
     * 监控，并进行失败重启
     * @param $cmdName
     * @param $validateFunc
     * @param null $validateTime
     * @return bool
     */
    public function monitor($cmdName,$validateFunc,$validateTime = null) {
        $validateData = $this->getFilenameAndValue($cmdName);
        $validateFunc($validateData);
        if($this->validate($validateData,$validateTime)) {
            return true;
        } else {
            $this->restart($cmdName);
        }
        return true;
    }



    /**
     * 重新启动命令
     * @param $cmdName
     */
    private function restart($cmdName) {
        $cmd = self::getCmdList($cmdName);
        $startTime = microtime(true);
        $closeNum = exec('ps -ef | grep \'' . $cmd . '\' | wc -l') - 2;
        exec('ps -ef | grep \'' . $cmd . '\' | grep -v grep | cut -c 9-15 | xargs kill -s 9 > /dev/null 2>&1');
        usleep(200000);
        exec('nohup ' . $cmd .' > /dev/null 2>&1 &');
        $newNum = 0;
        for($i=0;$i<10;++$i) {
            usleep(200000);
            $num = exec('ps -ef | grep \'' . $cmd . '\' | wc -l') - 2;
            if($newNum == $num) {
                break;
            }
            $newNum = $num;
        }
        // 添加重启日志
        $this->restartLog($cmdName,[
            'close' => $closeNum,
            'open'  => $newNum,
            'time'  => microtime(true) - $startTime,
        ]);
        return true;
    }


    /**
     * 获取文件及验证值
     * @param $functionName
     * @return string[]
     */
    private function getFilenameAndValue($functionName) {
        $filename = $this->validatePath . DIRECTORY_SEPARATOR . $functionName . '.log';
        $validateValue = microtime(true) . '_' . rand(100000,999999);
        return [
            'crontabType'  => 'validate',
            'file'  => $filename,
            'value' => $validateValue,
        ];
    }


    /**
     * 验证写入
     */
    static function validateWrite($validateData) {
        if(!is_array($validateData) || !array_key_exists('crontabType',$validateData) || $validateData['crontabType'] != 'validate') {
            return false;
        }
        try {
            file_put_contents($validateData['file'],$validateData['value']);
        }catch (\Exception $e) {
        }
        return true;
    }

    /**
     * 验证是否得到响应
     * @param $validateData 请求的参数
     * @param $time 验证时间长度，单位 秒
     * @return bool
     */
    private function validate($validateData,$time) {
        $startTime = microtime(true);
        $endTime = bcadd($startTime,$time,3);
        do{
            clearstatcache();
            if(file_exists($validateData['file']) && trim(file_get_contents($validateData['file'])) == $validateData['value']) {
                return true;
            }
            usleep(100000);
        }while(bccomp(microtime(true),$endTime,3) == -1);

        return false;
    }


    /**
     * 添加重启日志
     * @param $cmdName 指令对应名
     * @param $data
     */
    private function restartLog($cmdName,$data) {
        $filename = dirname($this->validatePath) . DIRECTORY_SEPARATOR . self::CONFIG['RESTART_LOG_NAME'];
        file_put_contents($filename,implode(' ',[date('Y-m-d H:i:s'),$cmdName,serialize($data)]) . "\n",FILE_APPEND);
    }

    /**
     * 错误日志
     * @param $cmdName cmd 名
     * @param $error    错误信息
     */
    private function errorLog($cmdName,$e) {
        $filename = dirname($this->validatePath) . DIRECTORY_SEPARATOR . self::CONFIG['ERROR_LOG_NAME'];
        file_put_contents($filename,implode(' ',[date('Y-m-d H:i:s'),$cmdName,$e->getMessage(),$e->getFile(),$e->getLine()]) . "\n",FILE_APPEND);
    }




    /**
     * 设置验证文件路径
     * @return bool
     */
    public function setValidatePath() {
        $this->validatePath = implode(DIRECTORY_SEPARATOR,[dirname(__FILE__),'crontab','run']);
        !is_dir($this->validatePath) && !mkdir($this->validatePath,0777,true);
        return true;
    }


    /**
     * 获取验证等待时长
     * @param string $cmdName cmd 名
     * @return float 单位 毫秒
     */
    static function getUtime(string $cmdName,$type = 'VALIDATE') {
        switch ($type) {
            case 'VALIDATE':
                $timeList = self::USLEEP_TIME_LIST['VALIDATE_LIST'];
                break;
            case 'WHILE':
                $timeList = self::USLEEP_TIME_LIST['WHILE_LIST'];
                break;
            case 'WAIT':
                $timeList = self::USLEEP_TIME_LIST['WAIT'];
                break;
            default:
                $timeList = self::USLEEP_TIME_LIST;
        }
        return $timeList[array_key_exists($cmdName,$timeList)?$cmdName:'DEFAULT'];
    }

    /**
     * 获取 env 值
     * @param string $name 模块下的名
     * @param string $default 默认值，注意，返回空也等于默认值
     * @param string $split 是否对结果按指定字符进行分割
     * @return false|string|string[] 返回字符串或分割后的数组
     */
    protected function env(string $name,$default = '',$split = '') {
        $value = trim(env($name));
        $value =  $value === ''?$default:$value;
        return empty($split)?$value:explode($split,$value);
    }

}
